Object.extend = function(destination, source) {
  for (var property in source) {
    if (source.hasOwnProperty(property)) {
      destination[property] = source[property];
    }
  }

  return destination;
};

var maybe = function(d, f) { return (typeof d == 'undefined') ? f : d; };

(function() {
  var po = org.polymaps;

  var applyStyle = function(e, css, value) {
    if (typeof value != 'undefined') {
      e.style(css, value);
    }
  };

  // Apply styles where appropriate.
  var styleKml = function(e) {
    // Adjust stroke width for current zoom level.
    var zoom_factor = Math.pow(2, e.tile.zoom - 12.0);
    e.features.map(function(f) {
      var e = d3.select(f.element);
      var s = f.data.style;
      applyStyle(e, "fill", s.fill);
      applyStyle(e, "fill-opacity", s.fill_opacity);
      applyStyle(e, "stroke", s.stroke);
      applyStyle(e, "stroke-width", s.stroke_width ? Math.max(1, s.stroke_width * zoom_factor) : undefined);
      applyStyle(e, "stroke-opacity", s.stroke_opacity ? Math.min(0.6, s.stroke_opacity) : undefined);
    });
  };

  // Replaces all labeled features with text.
  var applyLabels = function(e) {
    d3.select(e.tile.element).selectAll("text.point_label")
        .data(e.features.filter(function (f) { return f.data.properties.name; }))
      .enter()
        .append("text")
        .attr("class", "point_label")
        .attr("transform", function(f) { return f.element.getAttribute("transform"); })
        .style("fill", function(f) { return f.data.style.label.color; })
        .style("font-size", function(f) { return Math.floor(10 * f.data.style.label.scale); })
        .text(function(f) { return f.data.properties.name; })
        .each(function(f) {
          var e = f.element;
          e.parentNode.removeChild(e);
          f.element = this;
        });
  };

  // Replaces all xlink:href features with images.
  var applyIcons = function(e) {
    d3.select(e.tile.element).selectAll("image.point_icon")
        .data(e.features.filter(function (f) { return f.data.style.icon; }))
      .enter()
        .append("g")
        .attr("class", "point_icon")
        .attr("href", function(f) { return f.data.style.icon.href; })
        .attr("transform", function(f) { return f.element.getAttribute("transform"); })
        .attr("height", function(f) { return Math.floor(20 * f.data.style.icon.scale); })
        .attr("width", function(f) { return this.getAttribute("height"); })
        .each(function(f) {
          var e = f.element;
          e.parentNode.removeChild(e);
          f.element = this;
        })
        .append("image")
        .attr("xlink:href", function(f) { return f.data.style.icon.href; })
        .attr("height", function(f) { return Math.floor(20 * f.data.style.icon.scale); })
        .attr("width", function(f) { return this.getAttribute("height"); })
        .attr("transform", "translate(-10,-10)");
  };

  po.kml = function() {
    var kml = po.geoJson(fetch);

    kml.on("load", styleKml);
    kml.on("load", applyLabels);
    kml.on("load", applyIcons);

    function fetch(url, update) {
      return po.queue.xml(url, function(xml) {
        update(geoJson(xml));
      });
    }

    var types = {

      /**
       * For type "Point", the "coordinates" member must be a single position.
       */
      Point: function(e) {
        return {
          type: "Point",
          coordinates: e.getElementsByTagName("coordinates")[0]
            .textContent
            .split(",")
            .map(Number)
        };
      },

      /**
       * For type "Polygon", the "coordinates" member must be an array of
       * LinearRing coordinate arrays. For Polygons with multiple rings, the
       * first must be the exterior ring and any others must be interior rings
       * or holes.
       */
      Polygon: function(e) {
        var coordinates = Array.prototype.slice.call(e.getElementsByTagName("coordinates"));
        return {
          type: "Polygon",
          coordinates: coordinates.map(function(c) {
            return c.textContent.trim().split(/\s+/).map(function(a) {
              return a.split(",").slice(0, 2).map(Number);
            });
          })
        };
      },

      /**
       * For type "LineString", the "coordinates" member must be an array of two
       * or more positions.
       *
       * A LinearRing is closed LineString with 4 or more positions. The first
       * and last positions are equivalent (they represent equivalent points).
       * Though a LinearRing is not explicitly represented as a GeoJSON geometry
       * type, it is referred to in the Polygon geometry type definition.
       */
      LineString: function(e) {
        return {
          type: "LineString",
          coordinates: e.getElementsByTagName("coordinates")[0]
            .textContent
            .trim()
            .split(/\s+/)
            .map(function(a) { return a.split(",").slice(0, 2).map(Number); })
        };
      }

    };

    function geometry(e) {
      return e && e.tagName in types && types[e.tagName](e);
    }

    /**
     * Converts a KML-style ABGR color to a CSS color and alpha value.
     */
    function convertColorFromKML(d) {
      var abgr = parseInt(d, 16);
      var r = abgr & 0xFF;
      var g = (abgr >>> 8) & 0xFF;
      var b = (abgr >>> 16) & 0xFF;
      var a = (abgr >>> 24) & 0xFF;

      return {
        rgb: "rgb(" + r + "," + g + "," + b + ")",
        alpha: (a / 255.0)
      };
    }

    var styleTypes = {
      LineStyle: function(e) {
        var result = { "fill_opacity": 0.0 };
        var kmlColor = e.getElementsByTagName("color")[0].textContent;
        if (kmlColor) {
          var cssColor = convertColorFromKML(kmlColor);
          result.stroke = cssColor.rgb;
          result.stroke_opacity = cssColor.alpha;
        }
        var strokeWidth = e.getElementsByTagName("width")[0].textContent;
        if (strokeWidth) {
          result.stroke_width = 0 + strokeWidth;
        }
        return result;
      },

      PolyStyle: function(e) {
        var result = {};
        var kmlColor = e.getElementsByTagName("color")[0].textContent;
        if (kmlColor) {
          var cssColor = convertColorFromKML(kmlColor);
          result.fill = cssColor.rgb;
          result.fill_opacity = cssColor.alpha;
        }
        return result;
      },

      IconStyle: function(e) {
        var icons = e.getElementsByTagName("href");
        if (!icons[0]) {
          return;
        }

        return {
          "icon": {
            "href": maybe(icons[0].textContent, ""),
            "scale": maybe(e.getElementsByTagName("scale")[0].textContent, "1.0")
          }
        };
      },

      LabelStyle: function(e) {
        var result = {"label": { "scale": 1.0 }};
        var kmlColor = e.getElementsByTagName("color")[0].textContent;
        if (kmlColor) {
          var cssColor = convertColorFromKML(kmlColor);
          result.label.color = cssColor.rgb;
          result.label.opacity = cssColor.alpha;
        }
        var scale = e.getElementsByTagName("scale")[0].textContent;
        if (scale) {
          result.label.scale = 0 + scale;
        }
        return result;
      }
    };

    function style(c) {
      var r = {};
      for (var e = c.firstChild; e; e = e.nextSibling) {
        var s = e && e.tagName in styleTypes && styleTypes[e.tagName](e);
        if (s) {
          r = Object.extend(r, s);
        }
      }
    	return r;
    }

    function geoJson(xml) {
      var features = [];
      var placemarks = xml.getElementsByTagName("Placemark");

      for (var i = 0; i < placemarks.length; i++) {
        var e = placemarks[i];
        var f = {"id": e.getAttribute("id"), "properties": {}};

        for (var c = e.firstChild; c; c = c.nextSibling) {
          switch (c.tagName) {
            case "name": f.properties.name = c.textContent; continue;
            case "description": f.properties.description = c.textContent; continue;
            case "Style": f.style = style(c); continue;
          }

          var g = geometry(c);
          if (g) {
            f.geometry = g;
          }
        }

        if (f.geometry) {
          features.push(f);
        }
      }
      return {type: "FeatureCollection", features: features};
    }

    return kml;
  };
})();